1. Préambule
1.1 Problématique
1.2 Objectifs dans ce projet
2. Import des librairies et réglage de Pandas
3.Déclaration des fonctions
4.Import des données
5. Analyse du DataSet
5.1 Types des données
5.2 Dimensions du DataFrame data
5.3 Valeurs manquantes
5.4 Cardinalité de data
6. Classification des produits grâce à leurs données textuelles descriptives
6.1 Préparation, Analyse des Features Textuelles
6.1.1 Sélection des features à traiter
6.1.2 Fusion des colonnes 'product_name' et 'description'
6.1.3 Traitement de la main category
6.1.3.1 Extraction de la main category
6.1.3.2 Statistique sur la main category
6.1.3.2.1 Nombre d'articles par catégorie
6.1.3.2.2 Nombre de mots en moyenne par description et par titre
6.2 Traitement du texte
6.2.1 Nettoyage et Normalisation du texte
6.2.2 Lemmatisation du texte et Filtrage des Noms + Verbes
6.2.3 Graphique Nombre de Mots par Catégorie par Traitement
6.2.4 Création du dictionnaire dictTxt
6.2.5 Création des vecteurs pondérés avec TF-IDF
6.2.6 Réduction de dimension PCA / T-SNE
6.2.7 Affichage de la classification des descriptions selon leurs catégories
6.2.8 K-Means sur matrice T-SNE
6.2.9 Affichage de la classification des descriptions selon leurs clusters
6.2.10 Matrice de Confusion
6.2.11 Calcul du Score ARI
7. Classification des produits grâce à leurs images
7.1 Méthode SIFT
7.1.1 Création du dictionnaire dictImages
7.1.2 Ajout du path des images...
7.1.3 ...et de leurs catégories respectives
7.1.4 Calcul et regroupement de l'ensemble des descripteurs
7.1.5 A propos du pre-processing des images
7.1.6 Création du Bag of Virtual Words
7.1.7 Création des histogrammes
7.1.8 Réduction de dimension PCA + T-SNE
7.1.9 Affichage de la classification des images selon leurs catégories
7.1.10 K-Means sur matrice T-SNE
7.1.11 Affichage de la classification des images selon leurs clusters
7.1.12 Matrice de Confusion
7.1.13 Calcul du Score ARI
7.2 Méthode Transfert Learning avec VGG16
7.2.1 Installer Keras
7.2.2 Explication rapide de VGG16
7.2.3 Création du modèle pré-entrainé par Keras
7.2.4 Création du dictionnaire dictModel
7.2.5 Ajout du path des images...
7.2.6 ...et de leurs catégories respectives
7.2.7 Préparation des images
7.2.8 Extraction des histogrammes
7.2.9 Clustering PCA / T-SNE
7.2.10 Affichage de la classification des images selon leurs catégories
7.2.11 K-Means sur matrice T-SNE
7.2.12 Affichage de la classification des images selon leurs clusters
7.2.13 Matrice de Confusion
7.2.14 Calcul du Score ARI
8. Fusion des features Textes et Images
8.1 Création et initialisation du dictionnaire dictTxt
8.2 Fusion des vecteurs de caractéristiques de textes et d'images
8.3 Clustering PCA / T-SNE
8.4 Affichage de la classification des produits selon leurs catégories
8.5 K-Means sur matrice T-SNE
8.6 Graphique Description/Cluster
8.7 Matrice de Confusion
8.8 Calcul du Score ARI
9. Conclusion
L’entreprise "Place de marché”,
souhaite lancer une marketplace e-commerce.
Sur la place de marché, des vendeurs
proposent des articles à des acheteurs
en postant une photo et une description.
Pour l'instant, l'attribution de la catégorie
d'un article est effectuée manuellement par
les vendeurs et est donc peu fiable.
De plus, le volume des articles est
pour l’instant très petit.
Pour rendre l’expérience utilisateur
des vendeurs (faciliter la mise en ligne
de nouveaux articles) et des acheteurs (faciliter
la recherche de produits) la plus fluide
possible et dans l'optique d'un passage
à l'échelle, il devient nécessaire d'automatiser
cette tâche.
Il nous est demandé d'étudier la faisabilité
d'un moteur de classification des articles
en différentes catégories, avec un niveau
de précision suffisant.
Réaliser une première étude de faisabilité d'un moteur
de classification d'articles basé sur une image et une
description pour l'automatisation de l'attribution
de la catégorie de l'article.
Analyser le jeu de données en réalisant un prétraitement
des images et des descriptions des produits, une réduction
de dimension, puis un clustering.
Les résultats du clustering seront présentés sous la forme
d’une représentation en deux dimensions, qui illustrera
le fait que les caractéristiques extraites permettent
de regrouper des produits de même catégorie.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import cv2
import pickle
# !pip install texthero
import texthero as hero
from nltk import pos_tag, word_tokenize
from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans, MiniBatchKMeans, DBSCAN
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.metrics import adjusted_rand_score
from keras.applications.vgg16 import VGG16,preprocess_input
from tensorflow.keras import Model
# Changing Pandas settings for
# be able to display more rows and more columns.
pd.set_option("max_rows", 200)
pd.set_option("display.max_columns", 200)
pd.set_option('display.float_format', lambda x: '%.5f' % x)
J'ai décomposé en fonctions toutes les actions
clés réalisés dans ce projet afin :
L'ensemble des fonctions que j'ai écrit et utilisé
dans ce projet, pour les parties concernant
le traitement du texte, des images ou
du texte + images sont listés ci-dessous :
def first_product_category_tree(txtATraiter):
'''
Returns the first element of product_category_tree
'''
return txtATraiter.replace('["','')\
.replace('"]','')\
.replace(' >> ','>>')\
.split('>>')[0]
def lemma_filter_english_words(txtToLemmatize,
toSentence=True,
tagFilter=None):
'''
This function lemmatizes the sentence given as argument.
It returns the choice:
- a lemmatized sentence
- a list containing each term of the lematized sentence.
In addition, it is possible to communicate a list containing
the nature or function of the words to be returned,
the others will be ignored and deleted.
The list of terms available for <tagFilter> are:
- r': Adverb
- n': Name
- v': Verb
'''
from nltk.stem import WordNetLemmatizer
from nltk import pos_tag, word_tokenize
sentenceToReturn = []
wnl = WordNetLemmatizer()
# Tokenize et tag each word of the sentence
for word, tag in pos_tag(word_tokenize(txtToLemmatize)):
# first letter in low case for the lemmatize fonction parameter
wntag = tag[0].lower()
if tagFilter:
wntag = wntag if wntag in tagFilter else None
else:
wntag = wntag if wntag in ['r', 'n', 'v'] else None
if wntag:
lemma = wnl.lemmatize(word.lower(), wntag)
else:
if tagFilter:
continue
else:
lemma = word.lower()
sentenceToReturn.append(lemma)
if toSentence:
return ' '.join(word for word in sentenceToReturn)
else:
return sentenceToReturn
def contains_only_integers(tup):
'''
This function returns <True> if the tupple
input communiqué contains only <int>.
Returns <False> in all other cases.
'''
for item in tup:
if not isinstance(item, int):
return False
return True
def tuppleOfTwoInt(var=False, varName=''):
'''
Indicates whether the format of the argument <var>
is well communicated in the right format.
The expected formats are: int or tupple of 2 int.
If var is an int then the function returns
a tupple in the format (var,var)
If var is a tupple of 2 int, the function returns
the tuple <var> as is.
Returns False without an error message if <var> is equal to False.
Returns False with an error message indicating
that the format is not valid in all other cases.
'''
if varName:
varName = str.strip(varName)+' '
if isinstance(var, bool):
if var:
print(f'TuppleOfTwoInt : The format of the {varName}argument is incorrect')
return False
elif isinstance(var, int):
return (var,var)
elif isinstance(var, tuple):
if contains_only_integers(var):
if len(var) == 2:
return var
print(f'TuppleOfTwoInt : The format of the {varName}argument is incorrect')
return False
def preProcessingImage(img, resize=False, gray=False, gaus=False, gausKernel=5, equ=False):
'''
Returns an image <img>, previously processed.
Processing may include:
- Image resizing
- Switching to grey level
- Gaussian Blur
- Equalizer
The function accepts as input either
an image, or the path of an image.
'''
if isinstance(img, str):
# We consider that it is not an image but the path of the image
img = cv2.imread(img, cv2.IMREAD_COLOR)
resize = tuppleOfTwoInt(var=resize, varName='resize')
if resize:
img = cv2.resize(img, resize)
if gray:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
if gaus and tuppleOfTwoInt(var=gausKernel, varName='gausKernel'):
gausKernel = tuppleOfTwoInt(var=gausKernel, varName='gausKernel')
img = cv2.GaussianBlur(img, gausKernel, cv2.BORDER_DEFAULT)
if equ:
img = cv2.equalizeHist(img)
return img
def descriptorExtraction(img):
'''
Returns the SIFT descriptors
of the image or of the image path <img>
communicated as an argument.
'''
if isinstance(img, str):
# We consider that it is not an image but the path of the image
img = cv2.imread(img, cv2.IMREAD_COLOR)
img = preProcessingImage(img, gray=True, gaus=True, equ=True)
sift = cv2.xfeatures2d.SIFT_create() # Création de l'objet SIFT
_, des = sift.detectAndCompute(img,None)
return des
def groupAllDescriptors(dictData):
'''
Calculates a matrix containing all the descriptors
calculated from a series of images.
The images are read from the dictionary
communicated as argument with the key "path" that
contains a list (or equivalent) of accesses to each image.
The function creates the key "totalDes" containing
the descriptor matrix and then returns the dictionary.
'''
totalDes = []
for img in dictData['path']:
des = descriptorExtraction(img)
totalDes.extend(des)
dictData['totalDes'] = np.array(totalDes, dtype=np.int)
return dictData
def createKmeansOfBagOfVirtualWords(dictData):
'''
Returns a trained MiniBatchKMeans including
Labels represent virtual words
calculated from the matrix of descriptors
contained in the "totalDes" key of the dictionary
communicated as an argument.
The MiniBatchKMeans is contained in
the "kmeans" key of the dictionary.
The number of clusters is determined by
the square root of the number of descriptors.
the batch size argument is equal to 3 times
the number of images in total.
Returns the dictData dictionary.
'''
# definition of clustering parameters
nClusters = int(round(np.sqrt(dictData['totalDes'].shape[0]),0))
batchSize = dictData['path'].shape[0]*3
print('nClusters =',nClusters)
print('batchSize =',batchSize)
dictData['kmeans'] = MiniBatchKMeans(n_clusters=nClusters,
batch_size=batchSize).fit(dictData['totalDes'])
return dictData
def createMatrixOfImages(dictData, size=224):
'''
Creates a 4-dimensional matrix from
a list of images or path to the images.
The 4 dimensions of the matrix are
distributed as below:
- 1st dimension: Number of images
- 2nd and 3rd dimension: Length and width of the image
- 4th dimension: Depth of the image.
- Is equal to 3 if image is in color.
- Is equal to 1 if image is grayscale.
The function expects a dictionary as argument.
The communicated images must be present
in the "path" key of the dictionary.
This function is intended to return a matrix
that will be used as input for a VGG16 neural network.
This neural network expects as input fixed
size images, by default square images of 224px side.
This value can be modified with the argument <size>.
The array is created and added to the
dictionary in the key "arrayImg".
The function returns the dictionary.
'''
listImages = []
for img in dictData['path']:
img = preProcessingImage(img, resize=size)
img = preprocess_input(img)
listImages.append(img)
print(f'Dimensions of the image matrix: {np.array(listImages).shape}')
dictData['arrayImg'] = np.array(listImages)
return dictData
def createHistograms(dictData):
'''
This function calculates the normalized histograms
of the images (or path of the images) contained
in the "path" key of the dictionary communicated as argument.
The images are listed in a List or equivalent.
The normalization is performed from the 1st dimension
of the matrix containing the set of descriptors.
This matrix is stored in the "totalDes" key of the dictionary.
A numpy array containing the histograms is created
and added to the dictionary to the "histograms" key.
The function returns the dictionary.
'''
list_histo = []
for count, image in enumerate(dictData['path']):
print(f'image processing number {count+1}/{dictData["path"].shape[0]}')
# Vector of 0 equal in size to the number of unique labels in kmeans
histo = np.zeros(len(set(dictData['kmeans'].labels_)))
# Number of key points, for Histogram Normalisation
nkp = dictData['totalDes'].shape[0]
for d in descriptorExtraction(image):
idx = dictData['kmeans'].predict([d])
histo[idx] += 1/nkp
list_histo.append(histo)
dictData['histograms'] = np.array(list_histo)
print(f'Dimensions of the histogram matrix: {dictData["histograms"].shape}')
return dictData
def reductionTwoDimensionPCAandTSNE(dictData, col='histograms', pcaComponents=0.99):
'''
This function applies a PCA transformation
followed by a T-SNE transformation to a data set.
The choice of PCA preprocessing is chosen to reduce
T-SNE computation time.
The function expects a dictionary as argument.
The data to be processed can be accessed with
the key <col> of the dictionary.
The key <col> is given as argument.
By default, PCA keeps 99% of the variance.
It is possible to determine the parameter
<n_components> of PCA with the argument <pcaComponents>.
T-SNE is set to return the data set from
the PCA transformation, with only 2 output dimensions.
The result is added to the dictionary with the key "tsne".
The function returns the dictionary.
'''
from scipy.sparse import issparse
print(f'Treatment of the column {col}')
print(f'Dimension of the original data set: {dictData[col].shape}')
print('Start of PCA processing:')
if pcaComponents <= 1:
print(f'Variance preserved: {pcaComponents*100}%')
else:
print(f'Number of components conserved: {pcaComponents}')
pca = PCA(n_components=pcaComponents)
if issparse(dictData[col]):
X_pca = pca.fit_transform(dictData[col].toarray())
else:
X_pca = pca.fit_transform(dictData[col])
print(f'Data set size after PCA processing: {X_pca.shape}')
print('End of PCA processing.')
print(f'Start T-SNE processing:')
tsne = TSNE(n_components=2)
X_tsne = tsne.fit_transform(X_pca)
print(f'Size of the dataset after T-SNE processing: {X_tsne.shape}')
print(f'End of T-SNE processing.')
dictData['tsne'] = X_tsne
return dictData
def displayTheFourImagesAndTheirHistograms(dictData, idx):
'''
Displays the same image 4 times in
succession with its associated histogram.
The displayed image will be read from the index <idx>.
of the "path" key of the dictionary
communicated as an argument.
Each image is displayed with a different
treatment in this order:
1) No processing
2) GrayScale
3) GrayScale + Gaussian Blur
4) GrayScale + Gaussian Blur + Equalization
To display an image, the function calls
the <drawImageAndHistogram> function
with the appropriate parameters.
'''
drawImageAndHistogram(dictData,idx=idx,preprocessing=[])
drawImageAndHistogram(dictData,idx=idx,preprocessing=['gray'])
drawImageAndHistogram(dictData,idx=idx,preprocessing=['gray','gaus'])
drawImageAndHistogram(dictData,idx=idx,preprocessing=['gray','gaus','equ'])
def drawImageAndHistogram(dictData,idx=0,preprocessing=[]):
'''
Displays an image and its associated histogram.
The displayed image will be read from the index <idx>
of the "path" key of the dictionary given as argument.
Before displaying an image, a preprocessing list
can be applied to it.
The list of preprocessing to be performed must
be given in the argument <preprocessing>.
The list of possible arguments are:
- "gray" --> "Gray Scale"
- "gaus" --> "Gaussian Blur"
- "equ" --> "Equalize Histogram"
The title of the created image indicates
the list of preprocessing performed or
indicates "None" if no preprocessing has been chosen.
'''
gray=gaus=equ=False
if 'gray' in preprocessing:
gray=True
if 'gaus'in preprocessing:
gaus=True
if 'equ' in preprocessing:
equ=True
# Matching the terms of the transformation to display the graph title
dictTransformations={'gray':'Gray Scale',
'gaus':'Gaussian Blur',
'equ': 'Equalize Histogram'}
# We add an 's' to 'transformation' in the title
# of the graph if we apply several transformations
if len(preprocessing) > 1:
s='s'
else:
s=''
# Formatting of applied transformations
if preprocessing == []:
listTransformations = 'None'
else:
listTransformations = ' + '.join({ preprocessing: dictTransformations[preprocessing]\
for preprocessing in preprocessing }.values())
img = preProcessingImage(dictData['path'][idx],
gray=gray,
gaus=gaus,
gausKernel=5,
equ=equ)
# Creation of the graph title
graphTitle = ''.join([dictData['path'][idx].split('/')[-1],
'\nTransformation'+s+': ',listTransformations])
fig = plt.figure(figsize=(20,10))
fig.suptitle(graphTitle, fontsize=14)
ax1 = fig.add_subplot(121)
ax1.title.set_text('Analyzed image')
ax1.imshow(img, cmap='gray')
ax2 = fig.add_subplot(122)
ax2.title.set_text('Histogram')
ax2.hist(img.ravel(),256,[0,256])
plt.show()
def displayTSNESortedByColor(dictData,hue='category',figsize=(20,10)):
'''
Displays a scatterplot of a 2-dimensional matrix
contained in the key "tsne" of the dictionary
communicated as argument.
The idea is to display the result of a T-SNE
dimension reduction in 2 dimensions.
The color of the points is a function of the
argument <hue> communicated in argument and
refers to a key of the dictionary.
'''
plt.figure(figsize=figsize)
plt.title('Product classified by colors according to their '+hue,
fontsize=24)
cmap = sns.color_palette("tab10",
n_colors=dictData[hue].nunique())
sns.scatterplot(x=dictData['tsne'][:,0],
y=dictData['tsne'][:,1],
hue=dictData[hue],
palette=cmap)
sns.color_palette("rocket_r",
as_cmap=True)
plt.show()
def createKMeansLabelsFromTSNE(dictData, n_clusters=7):
'''
This function calculates the Labels of a KMeans.
The function expects a dictionary as argument.
The calculated labels are added to the
dictionary with the key "labels".
The number of clusters of the KMeans is
communicated as an entry in the argument "n_clusters".
The data to be clustered are present
in the dictionary with the key "tsne".
The idea of this function is to cluster
the data in 2 dimensions resulting from
a dimension reduction with T-SNE.
Returns the dictionary.
'''
dictData['labels'] = pd.Series(KMeans(n_clusters=n_clusters)\
.fit(dictData['tsne'])\
.labels_,
name='labels')
return dictData
def viewARIScore(dictData):
'''
Calculates and displays the ARI score of the data
coming from the "category" and "labels" keys of
the dictionary communicated as argument.
'''
print('ARI score between clustering according\nto categories and KMeans labels:',
adjusted_rand_score(dictData['category'],
dictData['labels']))
def showConfusionMatrix(dictData,
y_true='category',
y_pred='labels',
xlabel='Clusters',
ylabel='Product Categories'):
from sklearn.metrics import confusion_matrix
from sklearn.preprocessing import LabelEncoder
# We guard against cases where y_true or y_pred are not numbers.
le_true = LabelEncoder()
le_pred = LabelEncoder()
y_true= le_true.fit_transform(dictData[y_true])
y_pred = le_pred.fit_transform(dictData[y_pred])
cm = confusion_matrix(y_true, y_pred)
plt.figure(figsize=(8,8))
sns.heatmap(cm,
fmt="d",
annot=True,
linewidths=.5,
xticklabels=le_pred.classes_,
yticklabels=le_true.classes_)
plt.xlabel(xlabel, fontsize=16)
plt.ylabel(ylabel, fontsize=16)
plt.title('Confusion Matrix', fontsize=24)
plt.show()
def combineTwoMatricesInOne(dictData,
finalKey='vectors',
key1='tfidf',
key2='histograms'):
'''
This function joins 2 matrices into 1.
The matrices are joined on their horizontal axis.
Both matrices must have the same number of lines.
The generated matrix has the same number of lines
in input as in output.
The generated matrix has a number of columns equal
to the number of columns of the 1st matrix + the
number of columns of the 2nd matrix.
The function expects a dictionary as argument.
The matrices are present at the indexes of the
dictionary designated by the arguments <key1> and <key2>.
The new generated matrix is stored in the
dictionary at the key <finalKey> given as argument.
The function returns the dictionary.
'''
vectors = []
print('Dimensions of two matrices:')
print(f' Matrix n°1 -- Name: {key1} -- Shape: {dictData[key1].shape}')
print(f' Matrix n°2 -- Name: {key2} -- Shape: {dictData[key2].shape}')
for idx,vect in enumerate(dictData[key1]):
vectors.append(np.concatenate((vect.toarray()[0],
dictData[key2][idx])))
dictData[finalKey] = np.array(vectors)
print(f'Final matrix -- Name: {finalKey} -- Shape: {dictData[finalKey].shape}')
return dictData
data = pd.read_csv('Flipkart/flipkart_com-ecommerce_sample_1050.csv')
data.head(3).T
| 0 | 1 | 2 | |
|---|---|---|---|
| uniq_id | 55b85ea15a1536d46b7190ad6fff8ce7 | 7b72c92c2f6c40268628ec5f14c6d590 | 64d5d4a258243731dc7bbb1eef49ad74 |
| crawl_timestamp | 2016-04-30 03:22:56 +0000 | 2016-04-30 03:22:56 +0000 | 2016-04-30 03:22:56 +0000 |
| product_url | http://www.flipkart.com/elegance-polyester-mul... | http://www.flipkart.com/sathiyas-cotton-bath-t... | http://www.flipkart.com/eurospa-cotton-terry-f... |
| product_name | Elegance Polyester Multicolor Abstract Eyelet ... | Sathiyas Cotton Bath Towel | Eurospa Cotton Terry Face Towel Set |
| product_category_tree | ["Home Furnishing >> Curtains & Accessories >>... | ["Baby Care >> Baby Bath & Skin >> Baby Bath T... | ["Baby Care >> Baby Bath & Skin >> Baby Bath T... |
| pid | CRNEG7BKMFFYHQ8Z | BTWEGFZHGBXPHZUH | BTWEG6SHXTDB2A2Y |
| retail_price | 1899.00000 | 600.00000 | NaN |
| discounted_price | 899.00000 | 449.00000 | NaN |
| image | 55b85ea15a1536d46b7190ad6fff8ce7.jpg | 7b72c92c2f6c40268628ec5f14c6d590.jpg | 64d5d4a258243731dc7bbb1eef49ad74.jpg |
| is_FK_Advantage_product | False | False | False |
| description | Key Features of Elegance Polyester Multicolor ... | Specifications of Sathiyas Cotton Bath Towel (... | Key Features of Eurospa Cotton Terry Face Towe... |
| product_rating | No rating available | No rating available | No rating available |
| overall_rating | No rating available | No rating available | No rating available |
| brand | Elegance | Sathiyas | Eurospa |
| product_specifications | {"product_specification"=>[{"key"=>"Brand", "v... | {"product_specification"=>[{"key"=>"Machine Wa... | {"product_specification"=>[{"key"=>"Material",... |
data.dtypes
uniq_id object crawl_timestamp object product_url object product_name object product_category_tree object pid object retail_price float64 discounted_price float64 image object is_FK_Advantage_product bool description object product_rating object overall_rating object brand object product_specifications object dtype: object
data.shape
(1050, 15)
data.isnull().sum()
uniq_id 0 crawl_timestamp 0 product_url 0 product_name 0 product_category_tree 0 pid 0 retail_price 1 discounted_price 1 image 0 is_FK_Advantage_product 0 description 0 product_rating 0 overall_rating 0 brand 338 product_specifications 1 dtype: int64
data.nunique()
uniq_id 1050 crawl_timestamp 149 product_url 1050 product_name 1050 product_category_tree 642 pid 1050 retail_price 354 discounted_price 424 image 1050 is_FK_Advantage_product 2 description 1050 product_rating 27 overall_rating 27 brand 490 product_specifications 984 dtype: int64
Dans cette partie nous allons classer
et regrouper nos produits en autant de
groupes qu'il existe de catégories.
Nous réaliserons cela à travers
notre moteur de classification à partir
des textes qui servent à les décrire.
L'objectif et d'obtenir du moteur
de classification, le regroupement
le plus proche par rapport au
classement de référence initial.
Nous émettons l'hypothèse qu'un produit
peut-être attribué à une catégorie à
partir du vocubulaire utilisé dans sa description.
2 features permettent de décrire un produit :
Nous allons mettre en place le moteur de classification.
Pour optimiser ses performances, nous allons devoir
pré-traiter les textes.
L’objectif est, d’une part, de débarrasser les textes
de tous les éléments inutiles qui n’apportent pas
d’information permettant d’identifier le produit
et d’autre part de normaliser le texte afin qu’un
mot utilisé dans la description d’un produit ait
exactement la même orthographe dans les autres
descriptions des autres produits (gestion de la casse,
de l’accord en genre et en nombre, verbes conjugués
convertis dans leur forme à l’infinitif, etc.)
Alors seulement nous pourrons créer
un bag of words constitué des seuls éléments
de vocabulaire ayant une importance pour
décrire nos produits.
Un bag of words nous sert à décrire un document.
Chaque mot représente une feature, une caractéristique.
L'ensemble de plusieurs caractéristiques permet de
décrire un produit et ainsi déterminer sa catégorie.
Notre moteur de classification attribuera à
chaque produit un vecteur de caractéristiques,
qui sera fonction des termes descriptifs inclus
dans sa description et pondéré avec l’ensemble
des termes existants pour décrire
l’ensemble des produits de notre jeu de données.
Pour améliorer les performances et également
permettre de visualiser la clusterisation des produits
de notre moteur de classification, nous procèderons
à une réduction en 2 dimensions des vecteurs de caractéristiques.
Via une clusterisation, notre moteur regroupera ensemble
les produits qu’il estimera appartenir à la même catégorie.
Enfin, nous pourrons évaluer notre moteur de
classification en comparant le regroupement de
l’ensemble des produits réalisé par notre moteur de
classification avec la catégorie réelle de chaque produit.
Seules 3 features nous interresse dans cette partie :
df = data.copy()[['product_name','description','product_category_tree']]
df.head(2)
| product_name | description | product_category_tree | |
|---|---|---|---|
| 0 | Elegance Polyester Multicolor Abstract Eyelet ... | Key Features of Elegance Polyester Multicolor ... | ["Home Furnishing >> Curtains & Accessories >>... |
| 1 | Sathiyas Cotton Bath Towel | Specifications of Sathiyas Cotton Bath Towel (... | ["Baby Care >> Baby Bath & Skin >> Baby Bath T... |
Je fusionne le titre et la description du produit
pour ne garder qu'un unique champ contenant
l'ensemble des termes décrivant l'objet.
df['title_desc'] = df['product_name']+' '+df['description']
Chaque produit est caractérisé par la
catégorie qui lui est associé.
Il existe plusieurs niveaux de catégorie.
Pour l'exercice nous n'allons conserver
que la **catégorie principale** de chaque produit.
Création de la feature "category" qui contient
le nom de la catégorie principale de chaque produit :
df['category'] = df['product_category_tree']\
.apply(lambda row: first_product_category_tree(row))
df.drop('product_category_tree',
axis=1,
inplace=True)
df.head(2)
| product_name | description | title_desc | category | |
|---|---|---|---|---|
| 0 | Elegance Polyester Multicolor Abstract Eyelet ... | Key Features of Elegance Polyester Multicolor ... | Elegance Polyester Multicolor Abstract Eyelet ... | Home Furnishing |
| 1 | Sathiyas Cotton Bath Towel | Specifications of Sathiyas Cotton Bath Towel (... | Sathiyas Cotton Bath Towel Specifications of S... | Baby Care |
Les produits sont parfaitements répartis entre les catégories :
df['category'].value_counts()
Home Decor & Festive Needs 150 Home Furnishing 150 Watches 150 Computers 150 Baby Care 150 Kitchen & Dining 150 Beauty and Personal Care 150 Name: category, dtype: int64
Je crée 3 colonnes contenant
le nombre de mots dans les features :
# Assuming that a sentence with n words has n-1 spaces in it
df['count_description'] = df['description']\
.apply(lambda row: row.count(' ') + 1)
df['count_title'] = df['product_name']\
.apply(lambda row: row.count(' ') + 1)
df['count_title_description'] = \
df['title_desc'].apply(lambda row: row.count(' ') + 1)
cat = df.groupby('category').mean()[['count_description',
'count_title',
'count_title_description']]
plt.figure(figsize=(20,10))
sns.lineplot(x=cat.index,
y=cat['count_description'],
linewidth=3,
marker='o',
markersize=20,
label='description')
sns.lineplot(x=cat.index,
y=cat['count_title'],
linewidth=3,
marker='o',
markersize=20,
label='title')
sns.lineplot(x=cat.index,
y=cat['count_title_description'],
linewidth=3,
marker='o',
markersize=20,
label='description + title')
plt.legend()
plt.xlabel('Category',fontsize=16)
plt.xticks(rotation=20,ha='right')
plt.ylabel('Number of words',fontsize=16)
plt.title('Number of words per category', fontsize=24)
plt.grid()
plt.show()
Le nombre de mots utilisés en moyenne
pour décrire un produit est inégalement
réparti entre les catégories, pouvant
aller pratiquement du simple au double.
Cette étape est très importante pour l'optimisation
de notre moteur de classification.
Nous allons nettoyer et normaliser nos descriptions
en effectuant un ensemble d'opérations détaillées ci-dessous.
Pour cette étape, j'utilise la bilibliothèque texthero.
Utilisation de text hero:
La méthode clean effectue le traitement suivant :
J'affiche un exemple de description,
**avant** et **après** traitement :
**Avant** Normalisation :
print(f'Number of words : {df.title_desc[0].count(" ") + 1}\n')
print(df.title_desc[0])
Number of words : 237 Elegance Polyester Multicolor Abstract Eyelet Door Curtain Key Features of Elegance Polyester Multicolor Abstract Eyelet Door Curtain Floral Curtain,Elegance Polyester Multicolor Abstract Eyelet Door Curtain (213 cm in Height, Pack of 2) Price: Rs. 899 This curtain enhances the look of the interiors.This curtain is made from 100% high quality polyester fabric.It features an eyelet style stitch with Metal Ring.It makes the room environment romantic and loving.This curtain is ant- wrinkle and anti shrinkage and have elegant apparance.Give your home a bright and modernistic appeal with these designs. The surreal attention is sure to steal hearts. These contemporary eyelet and valance curtains slide smoothly so when you draw them apart first thing in the morning to welcome the bright sun rays you want to wish good morning to the whole world and when you draw them close in the evening, you create the most special moments of joyous beauty given by the soothing prints. Bring home the elegant curtain that softly filters light in your room so that you get the right amount of sunlight.,Specifications of Elegance Polyester Multicolor Abstract Eyelet Door Curtain (213 cm in Height, Pack of 2) General Brand Elegance Designed For Door Type Eyelet Model Name Abstract Polyester Door Curtain Set Of 2 Model ID Duster25 Color Multicolor Dimensions Length 213 cm In the Box Number of Contents in Sales Package Pack of 2 Sales Package 2 Curtains Body & Design Material Polyester
df['title_desc_clean'] = df['title_desc'].pipe(hero.clean)
**Après** Normalisation :
print(f'Number of words : {df["title_desc_clean"][0].count(" ") + 1}\n')
print(df['title_desc_clean'][0])
Number of words : 159 elegance polyester multicolor abstract eyelet door curtain key features elegance polyester multicolor abstract eyelet door curtain floral curtain elegance polyester multicolor abstract eyelet door curtain cm height pack price rs curtain enhances look interiors curtain made high quality polyester fabric features eyelet style stitch metal ring makes room environment romantic loving curtain ant wrinkle anti shrinkage elegant apparance give home bright modernistic appeal designs surreal attention sure steal hearts contemporary eyelet valance curtains slide smoothly draw apart first thing morning welcome bright sun rays want wish good morning whole world draw close evening create special moments joyous beauty given soothing prints bring home elegant curtain softly filters light room get right amount sunlight specifications elegance polyester multicolor abstract eyelet door curtain cm height pack general brand elegance designed door type eyelet model name abstract polyester door curtain set model id duster25 color multicolor dimensions length cm box number contents sales package pack sales package curtains body design material polyester
Le processus de « lemmatisation » consiste
à représenter les mots (ou « lemmes »)
sous leur forme canonique.
Par exemple pour un verbe, ce sera son infinitif.
Pour un nom, son masculin singulier.
L'idée étant encore une fois de ne conserver
que le sens des mots utilisés dans le corpus.
De plus, dans notre objectif de ne conserver que
des features permettant de décrire un objet,
nous allons filtrer les résultats pour ne conserver
que les Noms et Verbes des descriptions.
df['title_desc_lemma'] = \
df['title_desc_clean']\
.apply(lambda row: \
lemma_filter_english_words(row,
tagFilter=['n',
'v']))
**Avant** lemmatisation :
print(f'Number of words : {df.loc[0,"title_desc_clean"].count(" ") + 1}\n')
df.loc[0,'title_desc_clean']
Number of words : 159
'elegance polyester multicolor abstract eyelet door curtain key features elegance polyester multicolor abstract eyelet door curtain floral curtain elegance polyester multicolor abstract eyelet door curtain cm height pack price rs curtain enhances look interiors curtain made high quality polyester fabric features eyelet style stitch metal ring makes room environment romantic loving curtain ant wrinkle anti shrinkage elegant apparance give home bright modernistic appeal designs surreal attention sure steal hearts contemporary eyelet valance curtains slide smoothly draw apart first thing morning welcome bright sun rays want wish good morning whole world draw close evening create special moments joyous beauty given soothing prints bring home elegant curtain softly filters light room get right amount sunlight specifications elegance polyester multicolor abstract eyelet door curtain cm height pack general brand elegance designed door type eyelet model name abstract polyester door curtain set model id duster25 color multicolor dimensions length cm box number contents sales package pack sales package curtains body design material polyester'
**Après** lemmatisation :
print(f'Number of words : {df.loc[0,"title_desc_lemma"].count(" ") + 1}\n')
df.loc[0,'title_desc_lemma']
Number of words : 123
'elegance polyester multicolor eyelet door curtain feature elegance polyester multicolor eyelet door curtain curtain elegance polyester multicolor eyelet door curtain cm height price r curtain enhances look interior curtain make quality polyester fabric feature eyelet style stitch metal ring make room environment loving curtain wrinkle shrinkage apparance give home bright appeal design attention heart eyelet valance curtain slide thing morning bright sun ray want morning world draw evening create moment beauty give soothe print bring home elegant curtain filter light room get amount sunlight specification elegance polyester multicolor eyelet door curtain cm height brand elegance design door type eyelet model name polyester door curtain set model id duster25 color multicolor dimension length cm box number content sale package sale package curtain material polyester'
df.columns
Index(['product_name', 'description', 'title_desc', 'category',
'count_description', 'count_title', 'count_title_description',
'title_desc_clean', 'title_desc_lemma'],
dtype='object')
# Assuming that a sentence with n words has n-1 spaces in it
df['count_description'] = df['description']\
.apply(lambda row: row.count(' ') + 1)
df['count_title'] = df['product_name']\
.apply(lambda row: row.count(' ') + 1)
df['count_title_description'] = \
df['title_desc'].apply(lambda row: row.count(' ') + 1)
# Assuming that a sentence with n words has n-1 spaces in it
df['count_title_desc_clean'] = \
df['title_desc_clean'].apply(lambda row: row.count(' ') + 1)
df['count_title_desc_lemma'] = \
df['title_desc_lemma'].apply(lambda row: row.count(' ') + 1)
cat = df.groupby('category').mean()[['count_title_description',
'count_title_desc_clean',
'count_title_desc_lemma']]
plt.figure(figsize=(20,10))
sns.lineplot(x=cat.index,
y=cat['count_title_description'],
linewidth=3,
marker='o',
markersize=20,
label='without processing')
sns.lineplot(x=cat.index,
y=cat['count_title_desc_clean'],
linewidth=3,
marker='o',
markersize=20,
label='after cleaning ')
sns.lineplot(x=cat.index,
y=cat['count_title_desc_lemma'],
linewidth=3,
marker='o',
markersize=20,
label='after cleaning and lemmatization')
plt.legend()
plt.xlabel('Category',fontsize=16)
plt.xticks(rotation=20,ha='right')
plt.ylabel('Number of words',fontsize=16)
plt.title('Number of words per category per preprocessing', fontsize=24)
plt.grid()
plt.show()
100 - round(cat.count_title_desc_lemma.mean()/
cat.count_title_description.mean()*100,2)
49.61
En moyenne, les étapes de cleaning et
de lematisation suppriment 49.61%
de mots inutiles à la description de nos produits.
A partir de maintenant nous allons
manipuler différents objets.
Pour plus de simplicité,
j'utiliserai un dictionnaire
où chaque clé fera référence
à l'objet utilisé.
J'utiliserai également un ensemble
de fonctions que j'ai écrit et dont
vous trouverez les déclarations en
première partie de ce document.
Le plus souvent, ces fonctions prennent
en argument un dictionnaire et nous
retourne un dictionnaire enrichi
d'une nouvelle clé contenant l'objet
désiré (matrice tf-idf, t-sne, etc.).
J'ai essayé de choisir des noms
de fonction explicites, n'hésitez pas
à lire leur docstring si besoin.
dictTxt = {}
J'ajoute au dictionnaire la key "category"
contenant la feature "category" du DataFrame df.
dictTxt['category'] = df.category
Nous pouvons maintenant créer notre bag of words.
Il s'agit du sac de mots, sans ordre, contenant
l'ensemble du vocabulaire conservé et
qui décrit au mieux nos produits.
Chaque produit de notre jeu de données est
caractérisé par son vocabulaire et par le
nombre d'occurrences de chacun de ses mots
de vocabulaire.
Nous pourrions donc créer un vecteur,
où chaque élément de ce vecteur correspondrait
à un mot du bac of words, et où chaque valeur
de chaque élément du vecteur correspondrait
au nombre d'occurrences du mot dans
la description du produit.
Cette approche est intéressante, on émet l'hypothèse
que des catégories de produits rassemblent certains mots
de vocabulaire spécifiques plutôt que d'autres.
Cependant, certains mots peuvent être fréquents
et commun à plusieurs ou même à toutes les catégories.
Nous allons donc relativiser l'occurrence d'un mot
en fonction de sa présence dans l'ensemble
des descriptions des produits.
Pour réaliser cela je vais utiliser tf-idf avec sklearn.
Qu'est-ce que **TF-IDF** :
Term Frequency – Inverse Document Frequency
est une mesure qui permet, à partir d’un
ensemble de textes, de connaître l’importance
relative de chaque mot.
Idéalement, un terme apparaît très fréquemment
dans quelques textes seulement.
Les mots qui apparaissent dans presque tous les
documents ou très rarement n’ont que peu d’importance.
TF-IDF va nous permettre de créer une matrice de vecteurs,
où chaque élement du vecteur représente un mot
du bag of word et la valeur de chaque élément
du vecteur représente l'importance d'un mot
relativisé à l'ensemble des documents (ici,
à l'ensemble des descriptions des produits
du jeu de données.)
Création de la matrice tf-idf :
dictTxt['tfidf'] = TfidfVectorizer()\
.fit_transform(df['title_desc_lemma'])
Afin nous permettre de visualiser graphiquement
les regroupements de nos produits en fonction
de leur description mais également d'améliorer
les performances de notre moteur de classification,
nous allons appliquer une réduction de dimension
à notre matrice tf-idf.
L'objectif ici est de ramener nos vecteurs en 2 dimensions.
Nous allons utiliser pour cela l'algorithme T-SNE.
Cependant le charge peut-être très
lourde en termps de calcul.
Pour éviter cette problématique,
nous allons appliquer une première
réduction de dimension en appliquant
une Analyse en Composante Principale.
Nous ferons le choix de conserver 99%
de la variance afin de ne pas sacrifier
la qualité de nos données tout en
réduisant drastiquement le temps necessaire
à l'application de T-SNE pour la réduction
en 2 dimensions de nos vecteurs TF-IDF.
dictTxt = reductionTwoDimensionPCAandTSNE(dictTxt, col='tfidf')
Treatment of the column tfidf Dimension of the original data set: (1050, 3587) Start of PCA processing: Variance preserved: 99.0% Data set size after PCA processing: (1050, 791) End of PCA processing. Start T-SNE processing: Size of the dataset after T-SNE processing: (1050, 2) End of T-SNE processing.
Nous pouvons afficher notre matrice TF-IDF
maintenant réduite en 2 dimensions où
chaque point représente un produit :
plt.figure(figsize=(20,10))
plt.scatter(dictTxt['tsne'][:,0], dictTxt['tsne'][:,1])
plt.title('TF-IDF matrix in 2 dimensions')
plt.show()
On peut d'ores et déjà remarquer
des regroupements parmi les produits.
Ajoutons maintenant un peu de couleur.
Chaque produit est maintenant représenté
par une couleur représentant sa catégorie
parmi les 7 existantes.
displayTSNESortedByColor(dictTxt,hue='category')
Certaines catégories semblent très bien regroupées
comme "Watches" ou "Beauty and Personal Care".
D'autres sont plus diffuses comme "Home Furnising".
On peut émettre l'hypothèse que les groupes
correctement taggés ont un vocubulaire très
spécifique alors que les autres ont un vocubulaire
plus général susceptible d'être présent dans
plusieurs catégories.
L'idée à présent et de faire comme si
nous ne connaissions pas la réelle
catégorie des produits.
A partir de la matrice T-SNE, nous allons regrouper,
via l'algorithme K-Means les points selon 7 clusters.
Chaque cluster représente l'une des 7 catégories.
dictTxt = createKMeansLabelsFromTSNE(dictTxt)
Affichons maintenant les produits selon leurs clusters K-Means :
displayTSNESortedByColor(dictTxt,hue='labels')
Ici les clusters sont beaucoup plus
nets et bien délimités que précédement.
On constate que certains clusters
représentent assez fidèlement certaines
vraies catégories, encore une fois
comme "Watches" ou "Beauty and Personal Care".
Cependant les catégories les plus diffuses
ont bien entendu des produits mal
classés par K-Means.
Utilisons la matrice de confusion
pour visualiser les erreurs de classement.
showConfusionMatrix(dictTxt)
Les catégories ont la majorité de leurs produits
qui sont correctements classés.
Les clusters peuvent être attribués à leur catégorie
respective en identifiant, pour chaque catégorie,
le cluster qui la représente le plus.
La catégorie "Computers" est la catégorie
la plus difficile à classer. Presque la moitié des produits
est attribuée à tort à la catégorie "Beauty and Personal Care."
Pour mesurer l'erreur de notre moteur
de classification, nous allons utiliser
la métrique ARI via la fonction
adjusted_rand_score de sklearn.
L'adjusted_rand_score calcule une mesure
de similarité entre deux groupements
en considérant toutes les paires d'échantillons
et en comptant les paires qui sont assignées
dans le même groupement ou dans des groupements
différents dans les groupements prédits et réels.
Le score ARI va de 0 pour un étiquetage aléatoire
à 1 pour un étiquetage parfait.
viewARIScore(dictTxt)
ARI score between clustering according to categories and KMeans labels: 0.5258090491838915
Selon les essais nous obtenons
un score variant de 0.44 à 0.56.
C'est un score très encourageant.
Nous allons maintenant tenter de classer nos produits selon l'image associée à leur description.
Ici l'idée est similaire à l'analyse de texte :
Je détaille ci-dessous deux approches :
La scale-invariant feature transform (SIFT),
que l'on peut traduire par « transformation
de caractéristiques visuelles invariante à l'échelle »,
est un algorithme utilisé pour détecter et identifier
les éléments similaires entre différentes images
numériques (éléments de paysages, objets, personnes, etc.).
Il a été développé en 1999.
L'étape fondamentale de la méthode consiste
à calculer ce que l'on appelle les « descripteurs
SIFT » des images à étudier.
Il s'agit d'informations numériques dérivées
de l'analyse locale d'une image et qui caractérisent
le contenu visuel de cette image de la façon
la plus indépendante possible de l'échelle (« zoom »
et résolution du capteur), du cadrage, de l'angle
d'observation et de l'exposition (luminosité).
Ainsi, deux photographies d'un même objet auront
toutes les chances d'avoir des descripteurs SIFT
similaires, et ceci d'autant plus si les instants
de prise de vue et les angles de vue sont proches.
Voici le protocole que l'on va mettre en place,
pour créer notre moteur de classification des
produits à partir des images avec **SIFT** :
De la même manière que la partie texte,
j'utilise un dictionnaire pour stocker
et manipuler nos données.
dictImages = {}
J'importe le chemin complet (dossier + nom de fichier)
de chaque image sous forme de Series Pandas pour
qu'elles puissent êtres accessibles facilement.
dictImages['path'] = data['image'].apply(lambda row: 'Flipkart/Images/'+row)
J'importe également sous forme de Series Pandas
les catégories associées à chaque image.
Comme les index sont les mêmes, l'ordre
image/categorie est respecté.
dictImages['category'] = df['category']
On parcourt chacune des images referencées
dans notre dictionnaire dictImages (chaque
élement de la key "path")
Puis pour chaque image :
dictImages = groupAllDescriptors(dictImages)
dictImages['totalDes'].shape
(4185939, 128)
On obtient une matrice de 4 185 939 descripteurs.
Chaque descripteur est un vecteur de dimension (1,128).
J'affiche ci-dessous 1 image du jeu de données
et son histogramme associé, en appliquant
successivement les opérations de pré-processing suivant:
Attention, dans cette partie j'évoque le terme
***histogramme*** dans deux contextes différents :
L'histogramme affiché ci-dessous correspond
à l'histogramme classique que l'on retrouve
par exemple dans les logiciels d'édition de photographie.
En photographie, l’histogramme nous permet de visualiser
comment se distribuent les tons clairs et foncés
dans notre image.
Autrement dit, il donne des informations sur
l’exposition de notre image.
Plus simplement, à gauche de l’histogramme
sont représentés les pixels sombres, et à droite
les pixels clairs.
Plus il y a de pixels pour une tonalité
(très sombre, moyen, très clair, et tous les
intermédiaires), plus son “pic” sera élevé.
displayTheFourImagesAndTheirHistograms(dictImages,4)
Nous cherchons à trouver l'équivalent des mots,
en tant que features, que nous avions pour
l'analyse du texte.
Pour retrouver l'équivalent des mots,
nous allons créer des mots virtuels.
Grâce à l'algorithme MiniBatchKMeans
(une variante de l'algorithme KMeans
optimisé pour réduire le temps de calcul),
nous allons clusteriser notre matrice
de descripteurs calculée précedement.
Les descripteurs dont les caractéristiques
seront proches seront naturellement regroupés
dans des clusters identiques.
Chaque centroïde sera alors considéré
comme un mot virtuel.
La fonction **createKmeansOfBagOfVirtualWords**
définie les **deux hyperparamètres** de
**MiniBatchKMeans** de la manière suivante :
dictImages = createKmeansOfBagOfVirtualWords(dictImages)
nClusters = 2046 batchSize = 3150
dictImages['kmeans']
MiniBatchKMeans(batch_size=3150, n_clusters=2046)
On obtient un MiniBatchKMeans entrainé avec 2046 clusters.
Chaque centroïde de ces clusters représente les mots
de notre Bag of Virtual Words
Nous allons maintenant créer les histogrammes
de nos images.
Un histogramme est un vecteur de caractéristiques.
Nous allons réaliser pour nos images le même genre
d'opération qu'avec TF-IDF pour le texte de la
partie précedente.
Un histogramme est un vecteur où chaque élement
du vecteur correspond à un mot virtuel.
La dimension de notre matrice d'histogrammes est
donc égale à (Nombre d'images, Nombre de Clusters)
soit ici (1050,2046).
Pour renseigner chaque élement du vecteur d'une image
nous réalisons les opérations suivantes :
Une fois l'opération réalisée pour
tous les descripteurs d'une image,
l'histogramme est finalisé.
Nous réalisons cette opération
pour toutes les images.
Nous obtenons une matrice de
dimension (Nombre d'images, Nombre de mots virtuels
(c'est à dire de centroïdes dans le MiniBatchKMeans
entrainé) ce qui correspond dans notre cas à (1050,2046)
dictImages = createHistograms(dictImages)
image processing number 1/1050 image processing number 2/1050 image processing number 3/1050 image processing number 4/1050 image processing number 5/1050 image processing number 6/1050 image processing number 7/1050 image processing number 8/1050 image processing number 9/1050 image processing number 10/1050 image processing number 11/1050 image processing number 12/1050 image processing number 13/1050 image processing number 14/1050 image processing number 15/1050 image processing number 16/1050 image processing number 17/1050 image processing number 18/1050 image processing number 19/1050 image processing number 20/1050 image processing number 21/1050 image processing number 22/1050 image processing number 23/1050 image processing number 24/1050 image processing number 25/1050 image processing number 26/1050 image processing number 27/1050 image processing number 28/1050 image processing number 29/1050 image processing number 30/1050 image processing number 31/1050 image processing number 32/1050 image processing number 33/1050 image processing number 34/1050 image processing number 35/1050 image processing number 36/1050 image processing number 37/1050 image processing number 38/1050 image processing number 39/1050 image processing number 40/1050 image processing number 41/1050 image processing number 42/1050 image processing number 43/1050 image processing number 44/1050 image processing number 45/1050 image processing number 46/1050 image processing number 47/1050 image processing number 48/1050 image processing number 49/1050 image processing number 50/1050 image processing number 51/1050 image processing number 52/1050 image processing number 53/1050 image processing number 54/1050 image processing number 55/1050 image processing number 56/1050 image processing number 57/1050 image processing number 58/1050 image processing number 59/1050 image processing number 60/1050 image processing number 61/1050 image processing number 62/1050 image processing number 63/1050 image processing number 64/1050 image processing number 65/1050 image processing number 66/1050 image processing number 67/1050 image processing number 68/1050 image processing number 69/1050 image processing number 70/1050 image processing number 71/1050 image processing number 72/1050 image processing number 73/1050 image processing number 74/1050 image processing number 75/1050 image processing number 76/1050 image processing number 77/1050 image processing number 78/1050 image processing number 79/1050 image processing number 80/1050 image processing number 81/1050 image processing number 82/1050 image processing number 83/1050 image processing number 84/1050 image processing number 85/1050 image processing number 86/1050 image processing number 87/1050 image processing number 88/1050 image processing number 89/1050 image processing number 90/1050 image processing number 91/1050 image processing number 92/1050 image processing number 93/1050 image processing number 94/1050 image processing number 95/1050 image processing number 96/1050 image processing number 97/1050 image processing number 98/1050 image processing number 99/1050 image processing number 100/1050 image processing number 101/1050 image processing number 102/1050 image processing number 103/1050 image processing number 104/1050 image processing number 105/1050 image processing number 106/1050 image processing number 107/1050 image processing number 108/1050 image processing number 109/1050 image processing number 110/1050 image processing number 111/1050 image processing number 112/1050 image processing number 113/1050 image processing number 114/1050 image processing number 115/1050 image processing number 116/1050 image processing number 117/1050 image processing number 118/1050 image processing number 119/1050 image processing number 120/1050 image processing number 121/1050 image processing number 122/1050 image processing number 123/1050 image processing number 124/1050 image processing number 125/1050 image processing number 126/1050 image processing number 127/1050 image processing number 128/1050 image processing number 129/1050 image processing number 130/1050 image processing number 131/1050 image processing number 132/1050 image processing number 133/1050 image processing number 134/1050 image processing number 135/1050 image processing number 136/1050 image processing number 137/1050 image processing number 138/1050 image processing number 139/1050 image processing number 140/1050 image processing number 141/1050 image processing number 142/1050 image processing number 143/1050 image processing number 144/1050 image processing number 145/1050 image processing number 146/1050 image processing number 147/1050 image processing number 148/1050 image processing number 149/1050 image processing number 150/1050 image processing number 151/1050 image processing number 152/1050 image processing number 153/1050 image processing number 154/1050 image processing number 155/1050 image processing number 156/1050 image processing number 157/1050 image processing number 158/1050 image processing number 159/1050 image processing number 160/1050 image processing number 161/1050 image processing number 162/1050 image processing number 163/1050 image processing number 164/1050 image processing number 165/1050 image processing number 166/1050 image processing number 167/1050 image processing number 168/1050 image processing number 169/1050 image processing number 170/1050 image processing number 171/1050 image processing number 172/1050 image processing number 173/1050 image processing number 174/1050 image processing number 175/1050 image processing number 176/1050 image processing number 177/1050 image processing number 178/1050 image processing number 179/1050 image processing number 180/1050 image processing number 181/1050 image processing number 182/1050 image processing number 183/1050 image processing number 184/1050 image processing number 185/1050 image processing number 186/1050 image processing number 187/1050 image processing number 188/1050 image processing number 189/1050 image processing number 190/1050 image processing number 191/1050 image processing number 192/1050 image processing number 193/1050 image processing number 194/1050 image processing number 195/1050 image processing number 196/1050 image processing number 197/1050 image processing number 198/1050 image processing number 199/1050 image processing number 200/1050 image processing number 201/1050 image processing number 202/1050 image processing number 203/1050 image processing number 204/1050 image processing number 205/1050 image processing number 206/1050 image processing number 207/1050 image processing number 208/1050 image processing number 209/1050 image processing number 210/1050 image processing number 211/1050 image processing number 212/1050 image processing number 213/1050 image processing number 214/1050 image processing number 215/1050 image processing number 216/1050 image processing number 217/1050 image processing number 218/1050 image processing number 219/1050 image processing number 220/1050 image processing number 221/1050 image processing number 222/1050 image processing number 223/1050 image processing number 224/1050 image processing number 225/1050 image processing number 226/1050 image processing number 227/1050 image processing number 228/1050 image processing number 229/1050 image processing number 230/1050 image processing number 231/1050 image processing number 232/1050 image processing number 233/1050 image processing number 234/1050 image processing number 235/1050 image processing number 236/1050 image processing number 237/1050 image processing number 238/1050 image processing number 239/1050 image processing number 240/1050 image processing number 241/1050 image processing number 242/1050 image processing number 243/1050 image processing number 244/1050 image processing number 245/1050 image processing number 246/1050 image processing number 247/1050 image processing number 248/1050 image processing number 249/1050 image processing number 250/1050 image processing number 251/1050 image processing number 252/1050 image processing number 253/1050 image processing number 254/1050 image processing number 255/1050 image processing number 256/1050 image processing number 257/1050 image processing number 258/1050 image processing number 259/1050 image processing number 260/1050 image processing number 261/1050 image processing number 262/1050 image processing number 263/1050 image processing number 264/1050 image processing number 265/1050 image processing number 266/1050 image processing number 267/1050 image processing number 268/1050 image processing number 269/1050 image processing number 270/1050 image processing number 271/1050 image processing number 272/1050 image processing number 273/1050 image processing number 274/1050 image processing number 275/1050 image processing number 276/1050 image processing number 277/1050 image processing number 278/1050 image processing number 279/1050 image processing number 280/1050 image processing number 281/1050 image processing number 282/1050 image processing number 283/1050 image processing number 284/1050 image processing number 285/1050 image processing number 286/1050 image processing number 287/1050 image processing number 288/1050 image processing number 289/1050 image processing number 290/1050 image processing number 291/1050 image processing number 292/1050 image processing number 293/1050 image processing number 294/1050 image processing number 295/1050 image processing number 296/1050 image processing number 297/1050 image processing number 298/1050 image processing number 299/1050 image processing number 300/1050 image processing number 301/1050 image processing number 302/1050 image processing number 303/1050 image processing number 304/1050 image processing number 305/1050 image processing number 306/1050 image processing number 307/1050 image processing number 308/1050 image processing number 309/1050 image processing number 310/1050 image processing number 311/1050 image processing number 312/1050 image processing number 313/1050 image processing number 314/1050 image processing number 315/1050 image processing number 316/1050 image processing number 317/1050 image processing number 318/1050 image processing number 319/1050 image processing number 320/1050 image processing number 321/1050 image processing number 322/1050 image processing number 323/1050 image processing number 324/1050 image processing number 325/1050 image processing number 326/1050 image processing number 327/1050 image processing number 328/1050 image processing number 329/1050 image processing number 330/1050 image processing number 331/1050 image processing number 332/1050 image processing number 333/1050 image processing number 334/1050 image processing number 335/1050 image processing number 336/1050 image processing number 337/1050 image processing number 338/1050 image processing number 339/1050 image processing number 340/1050 image processing number 341/1050 image processing number 342/1050 image processing number 343/1050 image processing number 344/1050 image processing number 345/1050 image processing number 346/1050 image processing number 347/1050 image processing number 348/1050 image processing number 349/1050 image processing number 350/1050 image processing number 351/1050 image processing number 352/1050 image processing number 353/1050 image processing number 354/1050 image processing number 355/1050 image processing number 356/1050 image processing number 357/1050 image processing number 358/1050 image processing number 359/1050 image processing number 360/1050 image processing number 361/1050 image processing number 362/1050 image processing number 363/1050 image processing number 364/1050 image processing number 365/1050 image processing number 366/1050 image processing number 367/1050 image processing number 368/1050 image processing number 369/1050 image processing number 370/1050 image processing number 371/1050 image processing number 372/1050 image processing number 373/1050 image processing number 374/1050 image processing number 375/1050 image processing number 376/1050 image processing number 377/1050 image processing number 378/1050 image processing number 379/1050 image processing number 380/1050 image processing number 381/1050 image processing number 382/1050 image processing number 383/1050 image processing number 384/1050 image processing number 385/1050 image processing number 386/1050 image processing number 387/1050 image processing number 388/1050 image processing number 389/1050 image processing number 390/1050 image processing number 391/1050 image processing number 392/1050 image processing number 393/1050 image processing number 394/1050 image processing number 395/1050 image processing number 396/1050 image processing number 397/1050 image processing number 398/1050 image processing number 399/1050 image processing number 400/1050 image processing number 401/1050 image processing number 402/1050 image processing number 403/1050 image processing number 404/1050 image processing number 405/1050 image processing number 406/1050 image processing number 407/1050 image processing number 408/1050 image processing number 409/1050 image processing number 410/1050 image processing number 411/1050 image processing number 412/1050 image processing number 413/1050 image processing number 414/1050 image processing number 415/1050 image processing number 416/1050 image processing number 417/1050 image processing number 418/1050 image processing number 419/1050 image processing number 420/1050 image processing number 421/1050 image processing number 422/1050 image processing number 423/1050 image processing number 424/1050 image processing number 425/1050 image processing number 426/1050 image processing number 427/1050 image processing number 428/1050 image processing number 429/1050 image processing number 430/1050 image processing number 431/1050 image processing number 432/1050 image processing number 433/1050 image processing number 434/1050 image processing number 435/1050 image processing number 436/1050 image processing number 437/1050 image processing number 438/1050 image processing number 439/1050 image processing number 440/1050 image processing number 441/1050 image processing number 442/1050 image processing number 443/1050 image processing number 444/1050 image processing number 445/1050 image processing number 446/1050 image processing number 447/1050 image processing number 448/1050 image processing number 449/1050 image processing number 450/1050 image processing number 451/1050 image processing number 452/1050 image processing number 453/1050 image processing number 454/1050 image processing number 455/1050 image processing number 456/1050 image processing number 457/1050 image processing number 458/1050 image processing number 459/1050 image processing number 460/1050 image processing number 461/1050 image processing number 462/1050 image processing number 463/1050 image processing number 464/1050 image processing number 465/1050 image processing number 466/1050 image processing number 467/1050 image processing number 468/1050 image processing number 469/1050 image processing number 470/1050 image processing number 471/1050 image processing number 472/1050 image processing number 473/1050 image processing number 474/1050 image processing number 475/1050 image processing number 476/1050 image processing number 477/1050 image processing number 478/1050 image processing number 479/1050 image processing number 480/1050 image processing number 481/1050 image processing number 482/1050 image processing number 483/1050 image processing number 484/1050 image processing number 485/1050 image processing number 486/1050 image processing number 487/1050 image processing number 488/1050 image processing number 489/1050 image processing number 490/1050 image processing number 491/1050 image processing number 492/1050 image processing number 493/1050 image processing number 494/1050 image processing number 495/1050 image processing number 496/1050 image processing number 497/1050 image processing number 498/1050 image processing number 499/1050 image processing number 500/1050 image processing number 501/1050 image processing number 502/1050 image processing number 503/1050 image processing number 504/1050 image processing number 505/1050 image processing number 506/1050 image processing number 507/1050 image processing number 508/1050 image processing number 509/1050 image processing number 510/1050 image processing number 511/1050 image processing number 512/1050 image processing number 513/1050 image processing number 514/1050 image processing number 515/1050 image processing number 516/1050 image processing number 517/1050 image processing number 518/1050 image processing number 519/1050 image processing number 520/1050 image processing number 521/1050 image processing number 522/1050 image processing number 523/1050 image processing number 524/1050 image processing number 525/1050 image processing number 526/1050 image processing number 527/1050 image processing number 528/1050 image processing number 529/1050 image processing number 530/1050 image processing number 531/1050 image processing number 532/1050 image processing number 533/1050 image processing number 534/1050 image processing number 535/1050 image processing number 536/1050 image processing number 537/1050 image processing number 538/1050 image processing number 539/1050 image processing number 540/1050 image processing number 541/1050 image processing number 542/1050 image processing number 543/1050 image processing number 544/1050 image processing number 545/1050 image processing number 546/1050 image processing number 547/1050 image processing number 548/1050 image processing number 549/1050 image processing number 550/1050 image processing number 551/1050 image processing number 552/1050 image processing number 553/1050 image processing number 554/1050 image processing number 555/1050 image processing number 556/1050 image processing number 557/1050 image processing number 558/1050 image processing number 559/1050 image processing number 560/1050 image processing number 561/1050 image processing number 562/1050 image processing number 563/1050 image processing number 564/1050 image processing number 565/1050 image processing number 566/1050 image processing number 567/1050 image processing number 568/1050 image processing number 569/1050 image processing number 570/1050 image processing number 571/1050 image processing number 572/1050 image processing number 573/1050 image processing number 574/1050 image processing number 575/1050 image processing number 576/1050 image processing number 577/1050 image processing number 578/1050 image processing number 579/1050 image processing number 580/1050 image processing number 581/1050 image processing number 582/1050 image processing number 583/1050 image processing number 584/1050 image processing number 585/1050 image processing number 586/1050 image processing number 587/1050 image processing number 588/1050 image processing number 589/1050 image processing number 590/1050 image processing number 591/1050 image processing number 592/1050 image processing number 593/1050 image processing number 594/1050 image processing number 595/1050 image processing number 596/1050 image processing number 597/1050 image processing number 598/1050 image processing number 599/1050 image processing number 600/1050 image processing number 601/1050 image processing number 602/1050 image processing number 603/1050 image processing number 604/1050 image processing number 605/1050 image processing number 606/1050 image processing number 607/1050 image processing number 608/1050 image processing number 609/1050 image processing number 610/1050 image processing number 611/1050 image processing number 612/1050 image processing number 613/1050 image processing number 614/1050 image processing number 615/1050 image processing number 616/1050 image processing number 617/1050 image processing number 618/1050 image processing number 619/1050 image processing number 620/1050 image processing number 621/1050 image processing number 622/1050 image processing number 623/1050 image processing number 624/1050 image processing number 625/1050 image processing number 626/1050 image processing number 627/1050 image processing number 628/1050 image processing number 629/1050 image processing number 630/1050 image processing number 631/1050 image processing number 632/1050 image processing number 633/1050 image processing number 634/1050 image processing number 635/1050 image processing number 636/1050 image processing number 637/1050 image processing number 638/1050 image processing number 639/1050 image processing number 640/1050 image processing number 641/1050 image processing number 642/1050 image processing number 643/1050 image processing number 644/1050 image processing number 645/1050 image processing number 646/1050 image processing number 647/1050 image processing number 648/1050 image processing number 649/1050 image processing number 650/1050 image processing number 651/1050 image processing number 652/1050 image processing number 653/1050 image processing number 654/1050 image processing number 655/1050 image processing number 656/1050 image processing number 657/1050 image processing number 658/1050 image processing number 659/1050 image processing number 660/1050 image processing number 661/1050 image processing number 662/1050 image processing number 663/1050 image processing number 664/1050 image processing number 665/1050 image processing number 666/1050 image processing number 667/1050 image processing number 668/1050 image processing number 669/1050 image processing number 670/1050 image processing number 671/1050 image processing number 672/1050 image processing number 673/1050 image processing number 674/1050 image processing number 675/1050 image processing number 676/1050 image processing number 677/1050 image processing number 678/1050 image processing number 679/1050 image processing number 680/1050 image processing number 681/1050 image processing number 682/1050 image processing number 683/1050 image processing number 684/1050 image processing number 685/1050 image processing number 686/1050 image processing number 687/1050 image processing number 688/1050 image processing number 689/1050 image processing number 690/1050 image processing number 691/1050 image processing number 692/1050 image processing number 693/1050 image processing number 694/1050 image processing number 695/1050 image processing number 696/1050 image processing number 697/1050 image processing number 698/1050 image processing number 699/1050 image processing number 700/1050 image processing number 701/1050 image processing number 702/1050 image processing number 703/1050 image processing number 704/1050 image processing number 705/1050 image processing number 706/1050 image processing number 707/1050 image processing number 708/1050 image processing number 709/1050 image processing number 710/1050 image processing number 711/1050 image processing number 712/1050 image processing number 713/1050 image processing number 714/1050 image processing number 715/1050 image processing number 716/1050 image processing number 717/1050 image processing number 718/1050 image processing number 719/1050 image processing number 720/1050 image processing number 721/1050 image processing number 722/1050 image processing number 723/1050 image processing number 724/1050 image processing number 725/1050 image processing number 726/1050 image processing number 727/1050 image processing number 728/1050 image processing number 729/1050 image processing number 730/1050 image processing number 731/1050 image processing number 732/1050 image processing number 733/1050 image processing number 734/1050 image processing number 735/1050 image processing number 736/1050 image processing number 737/1050 image processing number 738/1050 image processing number 739/1050 image processing number 740/1050 image processing number 741/1050 image processing number 742/1050 image processing number 743/1050 image processing number 744/1050 image processing number 745/1050 image processing number 746/1050 image processing number 747/1050 image processing number 748/1050 image processing number 749/1050 image processing number 750/1050 image processing number 751/1050 image processing number 752/1050 image processing number 753/1050 image processing number 754/1050 image processing number 755/1050 image processing number 756/1050 image processing number 757/1050 image processing number 758/1050 image processing number 759/1050 image processing number 760/1050 image processing number 761/1050 image processing number 762/1050 image processing number 763/1050 image processing number 764/1050 image processing number 765/1050 image processing number 766/1050 image processing number 767/1050 image processing number 768/1050 image processing number 769/1050 image processing number 770/1050 image processing number 771/1050 image processing number 772/1050 image processing number 773/1050 image processing number 774/1050 image processing number 775/1050 image processing number 776/1050 image processing number 777/1050 image processing number 778/1050 image processing number 779/1050 image processing number 780/1050 image processing number 781/1050 image processing number 782/1050 image processing number 783/1050 image processing number 784/1050 image processing number 785/1050 image processing number 786/1050 image processing number 787/1050 image processing number 788/1050 image processing number 789/1050 image processing number 790/1050 image processing number 791/1050 image processing number 792/1050 image processing number 793/1050 image processing number 794/1050 image processing number 795/1050 image processing number 796/1050 image processing number 797/1050 image processing number 798/1050 image processing number 799/1050 image processing number 800/1050 image processing number 801/1050 image processing number 802/1050 image processing number 803/1050 image processing number 804/1050 image processing number 805/1050 image processing number 806/1050 image processing number 807/1050 image processing number 808/1050 image processing number 809/1050 image processing number 810/1050 image processing number 811/1050 image processing number 812/1050 image processing number 813/1050 image processing number 814/1050 image processing number 815/1050 image processing number 816/1050 image processing number 817/1050 image processing number 818/1050 image processing number 819/1050 image processing number 820/1050 image processing number 821/1050 image processing number 822/1050 image processing number 823/1050 image processing number 824/1050 image processing number 825/1050 image processing number 826/1050 image processing number 827/1050 image processing number 828/1050 image processing number 829/1050 image processing number 830/1050 image processing number 831/1050 image processing number 832/1050 image processing number 833/1050 image processing number 834/1050 image processing number 835/1050 image processing number 836/1050 image processing number 837/1050 image processing number 838/1050 image processing number 839/1050 image processing number 840/1050 image processing number 841/1050 image processing number 842/1050 image processing number 843/1050 image processing number 844/1050 image processing number 845/1050 image processing number 846/1050 image processing number 847/1050 image processing number 848/1050 image processing number 849/1050 image processing number 850/1050 image processing number 851/1050 image processing number 852/1050 image processing number 853/1050 image processing number 854/1050 image processing number 855/1050 image processing number 856/1050 image processing number 857/1050 image processing number 858/1050 image processing number 859/1050 image processing number 860/1050 image processing number 861/1050 image processing number 862/1050 image processing number 863/1050 image processing number 864/1050 image processing number 865/1050 image processing number 866/1050 image processing number 867/1050 image processing number 868/1050 image processing number 869/1050 image processing number 870/1050 image processing number 871/1050 image processing number 872/1050 image processing number 873/1050 image processing number 874/1050 image processing number 875/1050 image processing number 876/1050 image processing number 877/1050 image processing number 878/1050 image processing number 879/1050 image processing number 880/1050 image processing number 881/1050 image processing number 882/1050 image processing number 883/1050 image processing number 884/1050 image processing number 885/1050 image processing number 886/1050 image processing number 887/1050 image processing number 888/1050 image processing number 889/1050 image processing number 890/1050 image processing number 891/1050 image processing number 892/1050 image processing number 893/1050 image processing number 894/1050 image processing number 895/1050 image processing number 896/1050 image processing number 897/1050 image processing number 898/1050 image processing number 899/1050 image processing number 900/1050 image processing number 901/1050 image processing number 902/1050 image processing number 903/1050 image processing number 904/1050 image processing number 905/1050 image processing number 906/1050 image processing number 907/1050 image processing number 908/1050 image processing number 909/1050 image processing number 910/1050 image processing number 911/1050 image processing number 912/1050 image processing number 913/1050 image processing number 914/1050 image processing number 915/1050 image processing number 916/1050 image processing number 917/1050 image processing number 918/1050 image processing number 919/1050 image processing number 920/1050 image processing number 921/1050 image processing number 922/1050 image processing number 923/1050 image processing number 924/1050 image processing number 925/1050 image processing number 926/1050 image processing number 927/1050 image processing number 928/1050 image processing number 929/1050 image processing number 930/1050 image processing number 931/1050 image processing number 932/1050 image processing number 933/1050 image processing number 934/1050 image processing number 935/1050 image processing number 936/1050 image processing number 937/1050 image processing number 938/1050 image processing number 939/1050 image processing number 940/1050 image processing number 941/1050 image processing number 942/1050 image processing number 943/1050 image processing number 944/1050 image processing number 945/1050 image processing number 946/1050 image processing number 947/1050 image processing number 948/1050 image processing number 949/1050 image processing number 950/1050 image processing number 951/1050 image processing number 952/1050 image processing number 953/1050 image processing number 954/1050 image processing number 955/1050 image processing number 956/1050 image processing number 957/1050 image processing number 958/1050 image processing number 959/1050 image processing number 960/1050 image processing number 961/1050 image processing number 962/1050 image processing number 963/1050 image processing number 964/1050 image processing number 965/1050 image processing number 966/1050 image processing number 967/1050 image processing number 968/1050 image processing number 969/1050 image processing number 970/1050 image processing number 971/1050 image processing number 972/1050 image processing number 973/1050 image processing number 974/1050 image processing number 975/1050 image processing number 976/1050 image processing number 977/1050 image processing number 978/1050 image processing number 979/1050 image processing number 980/1050 image processing number 981/1050 image processing number 982/1050 image processing number 983/1050 image processing number 984/1050 image processing number 985/1050 image processing number 986/1050 image processing number 987/1050 image processing number 988/1050 image processing number 989/1050 image processing number 990/1050 image processing number 991/1050 image processing number 992/1050 image processing number 993/1050 image processing number 994/1050 image processing number 995/1050 image processing number 996/1050 image processing number 997/1050 image processing number 998/1050 image processing number 999/1050 image processing number 1000/1050 image processing number 1001/1050 image processing number 1002/1050 image processing number 1003/1050 image processing number 1004/1050 image processing number 1005/1050 image processing number 1006/1050 image processing number 1007/1050 image processing number 1008/1050 image processing number 1009/1050 image processing number 1010/1050 image processing number 1011/1050 image processing number 1012/1050 image processing number 1013/1050 image processing number 1014/1050 image processing number 1015/1050 image processing number 1016/1050 image processing number 1017/1050 image processing number 1018/1050 image processing number 1019/1050 image processing number 1020/1050 image processing number 1021/1050 image processing number 1022/1050 image processing number 1023/1050 image processing number 1024/1050 image processing number 1025/1050 image processing number 1026/1050 image processing number 1027/1050 image processing number 1028/1050 image processing number 1029/1050 image processing number 1030/1050 image processing number 1031/1050 image processing number 1032/1050 image processing number 1033/1050 image processing number 1034/1050 image processing number 1035/1050 image processing number 1036/1050 image processing number 1037/1050 image processing number 1038/1050 image processing number 1039/1050 image processing number 1040/1050 image processing number 1041/1050 image processing number 1042/1050 image processing number 1043/1050 image processing number 1044/1050 image processing number 1045/1050 image processing number 1046/1050 image processing number 1047/1050 image processing number 1048/1050 image processing number 1049/1050 image processing number 1050/1050 Dimensions of the histogram matrix: (1050, 2046)
Afin nous permettre de visualiser graphiquement
les regroupements de nos produits en fonction
de leur description mais également d'améliorer
les performances de notre moteur de classification,
nous allons appliquer une réduction de dimension
à notre matrice de vecteurs.
L'objectif ici est de ramener nos vecteurs en 2 dimensions.
Nous allons utiliser pour cela l'algorithme T-SNE.
Cependant la charge peut-être très
lourde en temps de calcul.
Pour éviter cette problématique,
nous allons appliquer une première
réduction de dimension en appliquant
une Analyse en Composante Principale.
Nous ferons le choix de conserver 99%
de la variance afin de ne pas sacrifier
la qualité de nos données tout en
réduisant drastiquement le temps nécessaire
à l'application de T-SNE pour la réduction
en 2 dimensions de nos histogrammes.
dictImages = reductionTwoDimensionPCAandTSNE(dictImages)
Treatment of the column histograms Dimension of the original data set: (1050, 2046) Start of PCA processing: Variance preserved: 99.0% Data set size after PCA processing: (1050, 53) End of PCA processing. Start T-SNE processing: Size of the dataset after T-SNE processing: (1050, 2) End of T-SNE processing.
Nous pouvons afficher notre matrice d'histogrammes
maintenant réduite en 2 dimensions où
chaque point représente un produit :
plt.figure(figsize=(20,10))
plt.scatter(dictImages['tsne'][:,0], dictImages['tsne'][:,1])
<matplotlib.collections.PathCollection at 0x1e180d922e0>
A ce stade, aucun regroupement de points ne semble se démarquer.
Ajoutons maintenant un peu de couleur.
Chaque produit est maintenant représenté
par une couleur représentant sa catégorie
parmi les 7 existantes.
displayTSNESortedByColor(dictImages,hue='category')
Il est très difficile de distinguer
des regroupements selon les catégories.
L'idée à présent et de faire comme si
nous ne connaissions pas la réelle
catégorie des produits.
A partir de la matrice T-SNE, nous allons regrouper,
via l'algorithme K-Means les points selon 7 clusters.
Chaque cluster représente l'une des 7 catégories.
dictImages = createKMeansLabelsFromTSNE(dictImages)
Affichons maintenant les produits selon leurs clusters K-Means :
displayTSNESortedByColor(dictImages,hue='labels')
Les points sont ici regroupées d'une manière beaucoup plus homogène.
Utilisons la matrice de confusion
pour visualiser les erreurs de classement.
showConfusionMatrix(dictImages)
Les catégories sont mal classées et il est impossible
d'associer avec certitude une catégorie à un numéro de cluster.
Pour mesurer l'erreur de notre moteur
de classification, nous allons utiliser
la métrique ARI via la fonction
adjusted_rand_score de sklearn.
L'adjusted_rand_score calcule une mesure
de similarité entre deux groupements
en considérant toutes les paires d'échantillons
et en comptant les paires qui sont assignées
dans le même groupement ou dans des groupements
différents dans les groupements prédits et réels.
Le score ARI va de 0 pour un etiquetage aléatoire
à 1 pour un étiquetage parfait.
viewARIScore(dictImages)
ARI score between clustering according to categories and KMeans labels: 0.06989068058163647
Nous obtenons un score d'environ 7.5%
Celà peut parraitre peu mais les images
n'étaient vraiment pas optimisées ni en nombre suffisant.
Ce score peut-être amélioré.
Nous allons dans cette partie utiliser Keras,
une bibliothèque de Deep Learning.
Nous utiliserons une version du réseau de neurones
convolutif VGG-Net nommé **VGG16**.
Le modèle VGG16 atteint une précision de 92,7%
dans le top 5 des tests d'ImageNet.
ImageNet est un ensemble de données de plus
de 15 millions d'images haute résolution
étiquetées appartenant à environ 22 000 catégories.
Keras nous fourni une version pré-entraînée de VGG16.
Pour mettre en place notre moteur de classification
d'images, nous appliquerons une technique
nommée Transfert Learning.
Simplement, le Transfert Learning consiste
à utiliser la connaissance déjà acquise
par un modèle entraîné (ici VGG16) pour l'adapter
à notre problématique.
Actuellement VGG16 permet de classer une image
parmi 1000 catégories différentes.
Nous souhaitons donc pouvoir utiliser
la puissance de **VGG16** mais adaptée à
notre problématique. C'est à dire pouvoir
classer une image non pas parmi 1000 catégories
mais parmi nos 7 catégories.
Avant d'aller plus loin,
vous devrez installer Keras.
Pour installer Keras :
Voici une vue des différentes couches de VGG16 :

VGG-16 est constitué de plusieurs couches,
dont 13 couches de convolution et 3 fully-connected.
Il prend en entrée une image en couleurs de taille 224 × 224 px
et la classifie dans une des 1000 classes.
Il renvoie donc un vecteur de taille 1000,
qui contient les probabilités d'appartenance
à chacune des classes.
Dans notre approche Transfert Learning, nous n'allons
pas utiliser la dernière couche softmax chargée de
classer l'image parmi l'une des 1000 catégories.
Nous récupérerons, pour chaque image, le vecteur
de dimension (1,1,4096), puis, comme pour la méthode SIFT,
ou pour le traitement du texte que nous avons
réalisé précedement dans ce projet, nous répéterons
les étapes de :
Nous allons dans un premier temps importer le modèle VGG16.
Puis nous créerons ensuite notre propre modèle,
qui sera une copie de VGG16 à l'exception de
la dernière couche **softmax**.
Création du modèle VGG-16 implémenté par Keras :
model = VGG16(weights="imagenet",
include_top=True, # test avec True
input_shape=(224, 224, 3))
On indique qu'on ne souhaite ré-entrainer
aucune des couches du modèle :
for layer in model.layers:
layer.trainable = False
On définit le nouveau modèle en
définissant son entrée et sa sortie :
En entrée, nous voulons utiliser l'entrée du modèle VGG16
En sortie, nous souhaitons obtenir le vecteur généré
par VGG16 juste avant la dernière couche softmax,
soit son avant-dernière couche.
new_model = Model(inputs=model.input, outputs=model.layers[-2].output)
Pour s'assurer que le nouveau modèle est
bien configuré, j'affiche son résumé que
je compare avec le modèle VGG16 initial :
Résumé du modèle VGG16 :
model.summary()
<bound method Model.summary of <tensorflow.python.keras.engine.functional.Functional object at 0x000001E1825C2A60>>
Résumé de notre nouveau modèle :
On remarque que la dernière couche
n'a pas été importée et que le modèle
ne va pas être ré-entrainé, même partiellement.
new_model.summary()
Model: "functional_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 224, 224, 3)] 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ flatten (Flatten) (None, 25088) 0 _________________________________________________________________ fc1 (Dense) (None, 4096) 102764544 _________________________________________________________________ fc2 (Dense) (None, 4096) 16781312 ================================================================= Total params: 134,260,544 Trainable params: 0 Non-trainable params: 134,260,544 _________________________________________________________________
Comme précedement, j'utilise un dictionnaire
pour stocker et manipuler nos données.
dictModel={}
J'importe le chemin complet (dossier + nom de fichier)
de chaque image sous forme de Series Pandas pour
qu'elles puissent être accessibles facilement.
dictModel['path'] = data.copy()['image'].apply(lambda row: 'Flipkart/Images/'+row)
J'importe également sous forme de Series Pandas
les catégories associées à chaque image.
Comme les index sont les mêmes, l'ordre
image/catégorie est respecté.
dictModel['category'] = df.copy()['category']
VGG16 attend en entrée une matrice d'images
en 4 dimensions composée de la façon suivante :
Préparons les images au bon format
pour notre Model basé sur VGG16 :
dictModel = createMatrixOfImages(dictModel)
Dimensions of the image matrix: (1050, 224, 224, 3)
Pour obtenir nos histogrammes avec notre
nouveau modèle, il suffit d'utiliser la méthode
predict en passant en argument la matrice
d'images en 4 dimensions.
Le modèle nous retourne une matrice
de (ici 1050) vecteurs, où chaque vecteur
est de dimension (1, 4096).
dictModel['histograms'] = new_model.predict(dictModel['arrayImg'])
dictModel['histograms'].shape
(1050, 4096)
Afin nous permettre de visualiser graphiquement
les regroupements de nos produits en fonction
de leur description mais également d'améliorer
les performances de notre moteur de classification,
nous allons appliquer une réduction de dimension
à notre matrice de vecteurs.
L'objectif ici est de ramener nos vecteurs en 2 dimensions.
Nous allons utiliser pour cela l'algorithme T-SNE.
Cependant la charge peut-être très
lourde en temps de calcul.
Pour éviter cette problématique,
nous allons appliquer une première
réduction de dimension en appliquant
une Analyse en Composante Principale.
Nous ferons le choix de conserver 99%
de la variance afin de ne pas sacrifier
la qualité de nos données tout en
réduisant drastiquement le temps nécessaire
à l'application de T-SNE pour la réduction
en 2 dimensions de nos histogrammes.
dictModel = reductionTwoDimensionPCAandTSNE(dictModel)
Treatment of the column histograms Dimension of the original data set: (1050, 4096) Start of PCA processing: Variance preserved: 99.0% Data set size after PCA processing: (1050, 800) End of PCA processing. Start T-SNE processing: Size of the dataset after T-SNE processing: (1050, 2) End of T-SNE processing.
Nous pouvons afficher notre matrice d'histogrammes
maintenant réduite en 2 dimensions où
chaque point représente un produit :
plt.figure(figsize=(20,10))
plt.scatter(dictModel['tsne'][:,0], dictModel['tsne'][:,1])
<matplotlib.collections.PathCollection at 0x1e1825d1ac0>
Les produits semblent nettement plus répartis
par groupe que lors de l'utilisation de SIFT.
Même si à ce stade il n'est pas possible de
savoir à quel point le moteur de classification
sera efficace, cette première représentation
avec notre model basé sur VGG16 semble plus
efficace que la méthode SIFT.
Ajoutons maintenant un peu de couleur.
Chaque produit est maintenant représenté
par une couleur représentant sa catégorie
parmi les 7 existantes.
displayTSNESortedByColor(dictModel,hue='category')
Comme pour le classement à partir de la description
texte des images, certaines catégories semblent
très bien regroupées comme "Watches" ou "Beauty and Personal Care".
D'autres sont plus diffuses comme "Home Furnising".
On peut émettre l'hypothèse que les groupes
correctement taggués ont des caractéristiques visuelles très
spécifiques alors que les autres ont des caractéristiques
plus générales et susceptibles d'être présentes dans
plusieurs catégories.
L'idée à présent et de faire comme si
nous ne connaissions pas la réelle
catégorie des produits.
A partir de la matrice T-SNE, nous allons regrouper,
via l'algorithme K-Means les points selon 7 clusters.
Chaque cluster représente l'une des 7 catégories.
dictModel = createKMeansLabelsFromTSNE(dictModel)
Affichons maintenant les produits selon leurs clusters K-Means :
displayTSNESortedByColor(dictModel,hue='labels')
Ici les clusters sont beaucoup plus
nets et bien délimités que précedement.
On constate que certains clusters
représentent assez fidèlement certaines
vraies catégories, encore une fois
comme "Watches" ou "Beauty and Personal Care".
Cependant les catégories les plus diffuses
ont bien entendu des produits mal
classés par K-Means.
Utilisons la matrice de confusion
pour visualiser les erreurs de classement.
showConfusionMatrix(dictImages)
Certaines catégories ont leurs produits
qui sont correctements classés.
D'autres catégories n'ont pas pu être associées
clairement à un cluster comme la catégorie "Baby Care".
Il est impossible d'associer avec certitude
une catégorie à un numéro de cluster.
Pour mesurer l'erreur de notre moteur
de classification, nous allons utiliser
la métrique ARI via la fonction
adjusted_rand_score de sklearn.
L'adjusted_rand_score calcule une mesure
de similarité entre deux groupements
en considérant toutes les paires d'échantillons
et en comptant les paires qui sont assignées
dans le même groupement ou dans des groupements
différents dans les groupements prédits et réels.
Le score ARI va de 0 pour un étiquetage aléatoire
à 1 pour un étiquetage parfait.
viewARIScore(dictModel)
ARI score between clustering according to categories and KMeans labels: 0.45450363293043006
Selon les essais nous obtenons
un score variant de 0.45 à 0.50.
C'est un score très encourageant.
Nous avons testé notre moteur de classification à partir
des textes de description des produits puis de leurs images,
nous allons tenter d'améliorer l'efficacité de notre
moteur de classification en fusionnant les vecteurs
de caractéristiques obtenus avec TF-IDF pour l'analyse textuelle,
puis les vecteurs issus de notre modèle basé sur **VGG16**
pour l'analyse des images.
Nous obtiendrons ainsi, pour chaque produit, un unique vecteur
de caractéristiques provenant à la fois des features
textes et images.
Les étapes de réalisation seront exactement
les mêmes que vu précedement.
Je ne rentrerai donc pas dans les détails dans cette partie.
Je crée un nouveau dictionnaire dans lequel j'importe :
dictTxtImages = {}
dictTxtImages['category'] = dictTxt['category']
dictTxtImages['tfidf'] = dictTxt['tfidf']
dictTxtImages['histograms'] = dictModel['histograms']
Je combine les deux matrices de vecteurs en une seule
et je stocke cette nouvelle matrice dans
la clé "vectors" du dictionnaire dictTxtImages.
dictTxtImages = combineTwoMatricesInOne(dictTxtImages)
Dimensions of two matrices: Matrix n°1 -- Name: tfidf -- Shape: (1050, 3587) Matrix n°2 -- Name: histograms -- Shape: (1050, 4096) Final matrix -- Name: vectors -- Shape: (1050, 7683)
On effectue la réduction des vecteurs en 2 dimensions :
dictTxtImages = reductionTwoDimensionPCAandTSNE(dictTxtImages)
Treatment of the column histograms Dimension of the original data set: (1050, 4096) Start of PCA processing: Variance preserved: 99.0% Data set size after PCA processing: (1050, 800) End of PCA processing. Start T-SNE processing: Size of the dataset after T-SNE processing: (1050, 2) End of T-SNE processing.
Nous pouvons afficher notre matrice d'histogrammes
maintenant réduite en 2 dimensions où
chaque point représente un produit :
plt.figure(figsize=(20,10))
plt.scatter(dictTxtImages['tsne'][:,0], dictTxtImages['tsne'][:,1])
<matplotlib.collections.PathCollection at 0x1e18451c250>
Ajoutons maintenant un peu de couleur.
Chaque produit est maintenant représenté
par une couleur représentant sa catégorie
parmi les 7 existantes.
displayTSNESortedByColor(dictTxtImages,hue='category')
Comme pour le classement à partir de la description
texte des images, certaines catégories semblent
très bien regroupées comme "Watches" ou "Beauty and Personal Care".
D'autres sont plus diffuses comme "Home Furnising".
L'idée est toujours de faire comme si
nous ne connaissions pas la réelle
catégorie des produits.
A partir de la matrice T-SNE, nous allons regrouper,
via l'algorithme K-Means les points selon 7 clusters.
Chaque cluster représente l'une des 7 catégories.
dictTxtImages = createKMeansLabelsFromTSNE(dictTxtImages)
Affichons maintenant les produits selon leurs clusters K-Means :
displayTSNESortedByColor(dictTxtImages,hue='labels')
Utilisons la matrice de confusion
pour visualiser les erreurs de classement.
showConfusionMatrix(dictImages)
Certaines catégories ont leurs produits
qui sont correctements classés.
D'autres catégories n'ont pas pu être associées
clairement à un cluster comme la catégorie "Baby Care
ou la catégorie "Kitchen & Dining".
Il est impossible d'associer avec certitude
une catégorie à un numéro de cluster.
Pour mesurer l'erreur de notre moteur
de classification, nous allons utiliser
la métrique ARI via la fonction
adjusted_rand_score de sklearn.
L'adjusted_rand_score calcule une mesure
de similarité entre deux groupements
en considérant toutes les paires d'échantillons
et en comptant les paires qui sont assignées
dans le même groupement ou dans des groupements
différents dans les groupements prédits et réels.
Le score ARI va de 0 pour un étiquetage aléatoire
à 1 pour un étiquetage parfait.
viewARIScore(dictTxtImages)
ARI score between clustering according to categories and KMeans labels: 0.44089257287108385
Nous obtenons un score de même ordre de grandeur
qu'avec utilisation seul du moteur de classification
textuel ou par image avec le model basé sur VGG16.
Nous avons successivement tenté de classer
nos produits à partir de leurs données
descriptives textuelles ainsi qu'à partir
de leurs images.
Nous avons ensuite tenté de classer les
produits en **fusionnant** les données textes
et images.
Les tests ont été réalisé à parti d'un
petit échantillon de 1050 produits.
A ce stade la classification à partir
des données textes (titre + description)
donne les résultats les plus satisfaisant.
L'analyse d'image n'apporte pas de précision
supplémentaire.
Cependant, la précision du moteur de classification
basé sur l'analyse des images peut être grandement
améliorés si nous décidons, par exemple, de ré-entrainer
un modèle basé sur VGG16 à partir d'une base
d'images correspondant à nos catégories de produits.
Globalement les résultats sont très encourageants
et nous permettent de donner un avis **positif**
sur la faisabilité du moteur de classification.